/*
 * Copyright 2014 The Netty Project
 *
 * The Netty Project licenses this file to you under the Apache License,
 * version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at:
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package io.netty.resolver.dns;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.AddressedEnvelope;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFactory;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoop;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.InternetProtocolFamily;
import io.netty.handler.codec.dns.DatagramDnsQueryEncoder;
import io.netty.handler.codec.dns.DatagramDnsResponse;
import io.netty.handler.codec.dns.DatagramDnsResponseDecoder;
import io.netty.handler.codec.dns.DnsQuestion;
import io.netty.handler.codec.dns.DnsRawRecord;
import io.netty.handler.codec.dns.DnsRecord;
import io.netty.handler.codec.dns.DnsRecordType;
import io.netty.handler.codec.dns.DnsResponse;
import io.netty.resolver.HostsFileEntriesResolver;
import io.netty.resolver.InetNameResolver;
import io.netty.resolver.ResolvedAddressTypes;
import io.netty.util.NetUtil;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.concurrent.FastThreadLocal;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import io.netty.util.internal.EmptyArrays;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.UnstableApi;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

import java.lang.reflect.Method;
import java.net.IDN;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static io.netty.util.internal.ObjectUtil.checkPositive;
import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero;

/**
 * A DNS-based {@link InetNameResolver}.
 */
@UnstableApi
public class DnsNameResolver extends InetNameResolver
{

    private static final InternalLogger logger = InternalLoggerFactory
            .getInstance(DnsNameResolver.class);

    private static final String LOCALHOST = "localhost";

    private static final InetAddress LOCALHOST_ADDRESS;

    private static final DnsRecord[] EMPTY_ADDITIONALS = new DnsRecord[0];

    private static final DnsRecordType[] IPV4_ONLY_RESOLVED_RECORD_TYPES = new DnsRecordType[]
    { DnsRecordType.A };

    private static final InternetProtocolFamily[] IPV4_ONLY_RESOLVED_PROTOCOL_FAMILIES = new InternetProtocolFamily[]
    { InternetProtocolFamily.IPv4 };

    private static final DnsRecordType[] IPV4_PREFERRED_RESOLVED_RECORD_TYPES = new DnsRecordType[]
    { DnsRecordType.A, DnsRecordType.AAAA };

    private static final InternetProtocolFamily[] IPV4_PREFERRED_RESOLVED_PROTOCOL_FAMILIES = new InternetProtocolFamily[]
    { InternetProtocolFamily.IPv4, InternetProtocolFamily.IPv6 };

    private static final DnsRecordType[] IPV6_ONLY_RESOLVED_RECORD_TYPES = new DnsRecordType[]
    { DnsRecordType.AAAA };

    private static final InternetProtocolFamily[] IPV6_ONLY_RESOLVED_PROTOCOL_FAMILIES = new InternetProtocolFamily[]
    { InternetProtocolFamily.IPv6 };

    private static final DnsRecordType[] IPV6_PREFERRED_RESOLVED_RECORD_TYPES = new DnsRecordType[]
    { DnsRecordType.AAAA, DnsRecordType.A };

    private static final InternetProtocolFamily[] IPV6_PREFERRED_RESOLVED_PROTOCOL_FAMILIES = new InternetProtocolFamily[]
    { InternetProtocolFamily.IPv6, InternetProtocolFamily.IPv4 };

    static final ResolvedAddressTypes DEFAULT_RESOLVE_ADDRESS_TYPES;

    static final String[] DEFAULT_SEARCH_DOMAINS;

    static
    {
        if (NetUtil.isIpV4StackPreferred())
        {
            DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_ONLY;
            LOCALHOST_ADDRESS = NetUtil.LOCALHOST4;
        }
        else
        {
            if (NetUtil.isIpV6AddressesPreferred())
            {
                DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV6_PREFERRED;
                LOCALHOST_ADDRESS = NetUtil.LOCALHOST6;
            }
            else
            {
                DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_PREFERRED;
                LOCALHOST_ADDRESS = NetUtil.LOCALHOST4;
            }
        }
    }

    static
    {
        String[] searchDomains;
        try
        {
            Class<?> configClass = Class
                    .forName("sun.net.dns.ResolverConfiguration");
            Method open = configClass.getMethod("open");
            Method nameservers = configClass.getMethod("searchlist");
            Object instance = open.invoke(null);

            @SuppressWarnings("unchecked")
            List<String> list = (List<String>) nameservers.invoke(instance);
            searchDomains = list.toArray(new String[list.size()]);
        }
        catch (Exception ignore)
        {
            // Failed to get the system name search domain list.
            searchDomains = EmptyArrays.EMPTY_STRINGS;
        }
        DEFAULT_SEARCH_DOMAINS = searchDomains;
    }

    private static final DatagramDnsResponseDecoder DECODER = new DatagramDnsResponseDecoder();

    private static final DatagramDnsQueryEncoder ENCODER = new DatagramDnsQueryEncoder();

    final DnsServerAddresses nameServerAddresses;

    final Future<Channel> channelFuture;

    final DatagramChannel ch;

    /**
     * Manages the {@link DnsQueryContext}s in progress and their query IDs.
     */
    final DnsQueryContextManager queryContextManager = new DnsQueryContextManager();

    /**
     * Cache for {@link #doResolve(String, Promise)} and
     * {@link #doResolveAll(String, Promise)}.
     */
    private final DnsCache resolveCache;

    private final DnsCache authoritativeDnsServerCache;

    private final FastThreadLocal<DnsServerAddressStream> nameServerAddrStream = new FastThreadLocal<DnsServerAddressStream>()
    {
        @Override
        protected DnsServerAddressStream initialValue() throws Exception
        {
            return nameServerAddresses.stream();
        }
    };

    private final long queryTimeoutMillis;

    private final int maxQueriesPerResolve;

    private final boolean traceEnabled;

    private final ResolvedAddressTypes resolvedAddressTypes;

    private final InternetProtocolFamily[] resolvedInternetProtocolFamilies;

    private final boolean recursionDesired;

    private final int maxPayloadSize;

    private final boolean optResourceEnabled;

    private final HostsFileEntriesResolver hostsFileEntriesResolver;

    private final DnsServerAddressStreamProvider dnsServerAddressStreamProvider;

    private final String[] searchDomains;

    private final int ndots;

    private final boolean supportsAAAARecords;

    private final boolean supportsARecords;

    private final InternetProtocolFamily preferredAddressType;

    private final DnsRecordType[] resolveRecordTypes;

    private final boolean decodeIdn;

    /**
     * Creates a new DNS-based name resolver that communicates with the
     * specified list of DNS servers.
     *
     * @param eventLoop the {@link EventLoop} which will perform the
     *        communication with the DNS servers
     * @param channelFactory the {@link ChannelFactory} that will create a
     *        {@link DatagramChannel}
     * @param nameServerAddresses the addresses of the DNS server. For each DNS
     *        query, a new stream is created from this to determine which DNS
     *        server should be contacted for the next retry in case of failure.
     * @param resolveCache the DNS resolved entries cache
     * @param authoritativeDnsServerCache the cache used to find the
     *        authoritative DNS server for a domain
     * @param queryTimeoutMillis timeout of each DNS query in millis
     * @param resolvedAddressTypes the preferred address types
     * @param recursionDesired if recursion desired flag must be set
     * @param maxQueriesPerResolve the maximum allowed number of DNS queries for
     *        a given name resolution
     * @param traceEnabled if trace is enabled
     * @param maxPayloadSize the capacity of the datagram packet buffer
     * @param optResourceEnabled if automatic inclusion of a optional records is
     *        enabled
     * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used
     *        to check for local aliases
     * @param dnsServerAddressStreamProvider The
     *        {@link DnsServerAddressStreamProvider} used to override the name
     *        servers for each hostname lookup.
     * @param searchDomains the list of search domain
     * @param ndots the ndots value
     * @param decodeIdn {@code true} if domain / host names should be decoded to
     *        unicode when received. See
     *        <a href="https://tools.ietf.org/html/rfc3492">rfc3492</a>.
     */
    public DnsNameResolver(EventLoop eventLoop,
            ChannelFactory<? extends DatagramChannel> channelFactory,
            DnsServerAddresses nameServerAddresses, final DnsCache resolveCache,
            DnsCache authoritativeDnsServerCache, long queryTimeoutMillis,
            ResolvedAddressTypes resolvedAddressTypes, boolean recursionDesired,
            int maxQueriesPerResolve, boolean traceEnabled, int maxPayloadSize,
            boolean optResourceEnabled,
            HostsFileEntriesResolver hostsFileEntriesResolver,
            DnsServerAddressStreamProvider dnsServerAddressStreamProvider,
            String[] searchDomains, int ndots, boolean decodeIdn)
    {
        super(eventLoop);
        checkNotNull(channelFactory, "channelFactory");
        this.nameServerAddresses = checkNotNull(nameServerAddresses,
                "nameServerAddresses");
        this.queryTimeoutMillis = checkPositive(queryTimeoutMillis,
                "queryTimeoutMillis");
        this.resolvedAddressTypes = resolvedAddressTypes != null
                ? resolvedAddressTypes : DEFAULT_RESOLVE_ADDRESS_TYPES;
        this.recursionDesired = recursionDesired;
        this.maxQueriesPerResolve = checkPositive(maxQueriesPerResolve,
                "maxQueriesPerResolve");
        this.traceEnabled = traceEnabled;
        this.maxPayloadSize = checkPositive(maxPayloadSize, "maxPayloadSize");
        this.optResourceEnabled = optResourceEnabled;
        this.hostsFileEntriesResolver = checkNotNull(hostsFileEntriesResolver,
                "hostsFileEntriesResolver");
        this.dnsServerAddressStreamProvider = checkNotNull(
                dnsServerAddressStreamProvider,
                "dnsServerAddressStreamProvider");
        this.resolveCache = checkNotNull(resolveCache, "resolveCache");
        this.authoritativeDnsServerCache = checkNotNull(
                authoritativeDnsServerCache, "authoritativeDnsServerCache");
        this.searchDomains = checkNotNull(searchDomains, "searchDomains")
                .clone();
        this.ndots = checkPositiveOrZero(ndots, "ndots");
        this.decodeIdn = decodeIdn;

        switch (this.resolvedAddressTypes)
        {
            case IPV4_ONLY:
                supportsAAAARecords = false;
                supportsARecords = true;
                resolveRecordTypes = IPV4_ONLY_RESOLVED_RECORD_TYPES;
                resolvedInternetProtocolFamilies = IPV4_ONLY_RESOLVED_PROTOCOL_FAMILIES;
                preferredAddressType = InternetProtocolFamily.IPv4;
                break;
            case IPV4_PREFERRED:
                supportsAAAARecords = true;
                supportsARecords = true;
                resolveRecordTypes = IPV4_PREFERRED_RESOLVED_RECORD_TYPES;
                resolvedInternetProtocolFamilies = IPV4_PREFERRED_RESOLVED_PROTOCOL_FAMILIES;
                preferredAddressType = InternetProtocolFamily.IPv4;
                break;
            case IPV6_ONLY:
                supportsAAAARecords = true;
                supportsARecords = false;
                resolveRecordTypes = IPV6_ONLY_RESOLVED_RECORD_TYPES;
                resolvedInternetProtocolFamilies = IPV6_ONLY_RESOLVED_PROTOCOL_FAMILIES;
                preferredAddressType = InternetProtocolFamily.IPv6;
                break;
            case IPV6_PREFERRED:
                supportsAAAARecords = true;
                supportsARecords = true;
                resolveRecordTypes = IPV6_PREFERRED_RESOLVED_RECORD_TYPES;
                resolvedInternetProtocolFamilies = IPV6_PREFERRED_RESOLVED_PROTOCOL_FAMILIES;
                preferredAddressType = InternetProtocolFamily.IPv6;
                break;
            default:
                throw new IllegalArgumentException(
                        "Unknown ResolvedAddressTypes " + resolvedAddressTypes);
        }

        Bootstrap b = new Bootstrap();
        b.group(executor());
        b.channelFactory(channelFactory);
        b.option(ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, true);
        final DnsResponseHandler responseHandler = new DnsResponseHandler(
                executor().<Channel> newPromise());
        b.handler(new ChannelInitializer<DatagramChannel>()
        {
            @Override
            protected void initChannel(DatagramChannel ch) throws Exception
            {
                ch.pipeline().addLast(DECODER, ENCODER, responseHandler);
            }
        });

        channelFuture = responseHandler.channelActivePromise;
        ch = (DatagramChannel) b.register().channel();
        ch.config().setRecvByteBufAllocator(
                new FixedRecvByteBufAllocator(maxPayloadSize));

        ch.closeFuture().addListener(new ChannelFutureListener()
        {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception
            {
                resolveCache.clear();
            }
        });
    }

    // Only here to override in unit tests.
    int dnsRedirectPort(@SuppressWarnings("unused") InetAddress server)
    {
        return DnsServerAddresses.DNS_PORT;
    }

    /**
     * Returns the resolution cache.
     */
    public DnsCache resolveCache()
    {
        return resolveCache;
    }

    /**
     * Returns the cache used for authoritative DNS servers for a domain.
     */
    public DnsCache authoritativeDnsServerCache()
    {
        return authoritativeDnsServerCache;
    }

    /**
     * Returns the timeout of each DNS query performed by this resolver (in
     * milliseconds). The default value is 5 seconds.
     */
    public long queryTimeoutMillis()
    {
        return queryTimeoutMillis;
    }

    /**
     * Returns the {@link ResolvedAddressTypes} resolved by
     * {@link #resolve(String)}. The default value depends on the value of the
     * system property {@code "java.net.preferIPv6Addresses"}.
     */
    public ResolvedAddressTypes resolvedAddressTypes()
    {
        return resolvedAddressTypes;
    }

    InternetProtocolFamily[] resolvedInternetProtocolFamiliesUnsafe()
    {
        return resolvedInternetProtocolFamilies;
    }

    final String[] searchDomains()
    {
        return searchDomains;
    }

    final int ndots()
    {
        return ndots;
    }

    final boolean supportsAAAARecords()
    {
        return supportsAAAARecords;
    }

    final boolean supportsARecords()
    {
        return supportsARecords;
    }

    final InternetProtocolFamily preferredAddressType()
    {
        return preferredAddressType;
    }

    final DnsRecordType[] resolveRecordTypes()
    {
        return resolveRecordTypes;
    }

    final boolean isDecodeIdn()
    {
        return decodeIdn;
    }

    /**
     * Returns {@code true} if and only if this resolver sends a DNS query with
     * the RD (recursion desired) flag set. The default value is {@code true}.
     */
    public boolean isRecursionDesired()
    {
        return recursionDesired;
    }

    /**
     * Returns the maximum allowed number of DNS queries to send when resolving
     * a host name. The default value is {@code 8}.
     */
    public int maxQueriesPerResolve()
    {
        return maxQueriesPerResolve;
    }

    /**
     * Returns if this resolver should generate the detailed trace information
     * in an exception message so that it is easier to understand the cause of
     * resolution failure. The default value if {@code true}.
     */
    public boolean isTraceEnabled()
    {
        return traceEnabled;
    }

    /**
     * Returns the capacity of the datagram packet buffer (in bytes). The
     * default value is {@code 4096} bytes.
     */
    public int maxPayloadSize()
    {
        return maxPayloadSize;
    }

    /**
     * Returns the automatic inclusion of a optional records that tries to give
     * the remote DNS server a hint about how much data the resolver can read
     * per response is enabled.
     */
    public boolean isOptResourceEnabled()
    {
        return optResourceEnabled;
    }

    /**
     * Returns the component that tries to resolve hostnames against the hosts
     * file prior to asking to remotes DNS servers.
     */
    public HostsFileEntriesResolver hostsFileEntriesResolver()
    {
        return hostsFileEntriesResolver;
    }

    /**
     * Closes the internal datagram channel used for sending and receiving DNS
     * messages, and clears all DNS resource records from the cache. Attempting
     * to send a DNS query or to resolve a domain name will fail once this
     * method has been called.
     */
    @Override
    public void close()
    {
        if (ch.isOpen())
        {
            ch.close();
        }
    }

    @Override
    protected EventLoop executor()
    {
        return (EventLoop) super.executor();
    }

    private InetAddress resolveHostsFileEntry(String hostname)
    {
        if (hostsFileEntriesResolver == null)
        {
            return null;
        }
        else
        {
            InetAddress address = hostsFileEntriesResolver.address(hostname,
                    resolvedAddressTypes);
            if (address == null && PlatformDependent.isWindows()
                    && LOCALHOST.equalsIgnoreCase(hostname))
            {
                // If we tried to resolve localhost we need workaround that
                // windows removed localhost from its
                // hostfile in later versions.
                // See https://github.com/netty/netty/issues/5386
                return LOCALHOST_ADDRESS;
            }
            return address;
        }
    }

    /**
     * Resolves the specified name into an address.
     *
     * @param inetHost the name to resolve
     * @param additionals additional records ({@code OPT})
     *
     * @return the address as the result of the resolution
     */
    public final Future<InetAddress> resolve(String inetHost,
            Iterable<DnsRecord> additionals)
    {
        return resolve(inetHost, additionals,
                executor().<InetAddress> newPromise());
    }

    /**
     * Resolves the specified name into an address.
     *
     * @param inetHost the name to resolve
     * @param additionals additional records ({@code OPT})
     * @param promise the {@link Promise} which will be fulfilled when the name
     *        resolution is finished
     *
     * @return the address as the result of the resolution
     */
    public final Future<InetAddress> resolve(String inetHost,
            Iterable<DnsRecord> additionals, Promise<InetAddress> promise)
    {
        checkNotNull(promise, "promise");
        DnsRecord[] additionalsArray = toArray(additionals, true);
        try
        {
            doResolve(inetHost, additionalsArray, promise, resolveCache);
            return promise;
        }
        catch (Exception e)
        {
            return promise.setFailure(e);
        }
    }

    /**
     * Resolves the specified host name and port into a list of address.
     *
     * @param inetHost the name to resolve
     * @param additionals additional records ({@code OPT})
     *
     * @return the list of the address as the result of the resolution
     */
    public final Future<List<InetAddress>> resolveAll(String inetHost,
            Iterable<DnsRecord> additionals)
    {
        return resolveAll(inetHost, additionals,
                executor().<List<InetAddress>> newPromise());
    }

    /**
     * Resolves the specified host name and port into a list of address.
     *
     * @param inetHost the name to resolve
     * @param additionals additional records ({@code OPT})
     * @param promise the {@link Promise} which will be fulfilled when the name
     *        resolution is finished
     *
     * @return the list of the address as the result of the resolution
     */
    public final Future<List<InetAddress>> resolveAll(String inetHost,
            Iterable<DnsRecord> additionals, Promise<List<InetAddress>> promise)
    {
        checkNotNull(promise, "promise");
        DnsRecord[] additionalsArray = toArray(additionals, true);
        try
        {
            doResolveAll(inetHost, additionalsArray, promise, resolveCache);
            return promise;
        }
        catch (Exception e)
        {
            return promise.setFailure(e);
        }
    }

    @Override
    protected void doResolve(String inetHost, Promise<InetAddress> promise)
            throws Exception
    {
        doResolve(inetHost, EMPTY_ADDITIONALS, promise, resolveCache);
    }

    private static DnsRecord[] toArray(Iterable<DnsRecord> additionals,
            boolean validateType)
    {
        checkNotNull(additionals, "additionals");
        if (additionals instanceof Collection)
        {
            Collection<DnsRecord> records = (Collection<DnsRecord>) additionals;
            for (DnsRecord r : additionals)
            {
                validateAdditional(r, validateType);
            }
            return records.toArray(new DnsRecord[records.size()]);
        }

        Iterator<DnsRecord> additionalsIt = additionals.iterator();
        if (!additionalsIt.hasNext())
        {
            return EMPTY_ADDITIONALS;
        }
        List<DnsRecord> records = new ArrayList<DnsRecord>();
        do
        {
            DnsRecord r = additionalsIt.next();
            validateAdditional(r, validateType);
            records.add(r);
        }
        while (additionalsIt.hasNext());

        return records.toArray(new DnsRecord[records.size()]);
    }

    private static void validateAdditional(DnsRecord record,
            boolean validateType)
    {
        checkNotNull(record, "record");
        if (validateType && record instanceof DnsRawRecord)
        {
            throw new IllegalArgumentException(
                    "DnsRawRecord implementations not allowed: " + record);
        }
    }

    private InetAddress loopbackAddress()
    {
        return preferredAddressType() == InternetProtocolFamily.IPv4
                ? NetUtil.LOCALHOST4 : NetUtil.LOCALHOST6;
    }

    /**
     * Hook designed for extensibility so one can pass a different cache on each
     * resolution attempt instead of using the global one.
     */
    protected void doResolve(String inetHost, DnsRecord[] additionals,
            Promise<InetAddress> promise, DnsCache resolveCache)
            throws Exception
    {
        if (inetHost == null || inetHost.isEmpty())
        {
            // If an empty hostname is used we should use "localhost", just like
            // InetAddress.getByName(...) does.
            promise.setSuccess(loopbackAddress());
            return;
        }
        final byte[] bytes = NetUtil
                .createByteArrayFromIpAddressString(inetHost);
        if (bytes != null)
        {
            // The inetHost is actually an ipaddress.
            promise.setSuccess(InetAddress.getByAddress(bytes));
            return;
        }

        final String hostname = hostname(inetHost);

        InetAddress hostsFileEntry = resolveHostsFileEntry(hostname);
        if (hostsFileEntry != null)
        {
            promise.setSuccess(hostsFileEntry);
            return;
        }

        if (!doResolveCached(hostname, additionals, promise, resolveCache))
        {
            doResolveUncached(hostname, additionals, promise, resolveCache);
        }
    }

    private boolean doResolveCached(String hostname, DnsRecord[] additionals,
            Promise<InetAddress> promise, DnsCache resolveCache)
    {
        final List<DnsCacheEntry> cachedEntries = resolveCache.get(hostname,
                additionals);
        if (cachedEntries == null || cachedEntries.isEmpty())
        {
            return false;
        }

        InetAddress address = null;
        Throwable cause = null;
        synchronized (cachedEntries)
        {
            final int numEntries = cachedEntries.size();
            assert numEntries > 0;

            if (cachedEntries.get(0).cause() != null)
            {
                cause = cachedEntries.get(0).cause();
            }
            else
            {
                // Find the first entry with the preferred address type.
                for (InternetProtocolFamily f : resolvedInternetProtocolFamilies)
                {
                    for (int i = 0; i < numEntries; i++)
                    {
                        final DnsCacheEntry e = cachedEntries.get(i);
                        if (f.addressType().isInstance(e.address()))
                        {
                            address = e.address();
                            break;
                        }
                    }
                }
            }
        }

        if (address != null)
        {
            trySuccess(promise, address);
            return true;
        }
        if (cause != null)
        {
            tryFailure(promise, cause);
            return true;
        }
        return false;
    }

    private static <T> void trySuccess(Promise<T> promise, T result)
    {
        if (!promise.trySuccess(result))
        {
            logger.warn("Failed to notify success ({}) to a promise: {}",
                    result, promise);
        }
    }

    private static void tryFailure(Promise<?> promise, Throwable cause)
    {
        if (!promise.tryFailure(cause))
        {
            logger.warn("Failed to notify failure to a promise: {}", promise,
                    cause);
        }
    }

    private void doResolveUncached(String hostname, DnsRecord[] additionals,
            Promise<InetAddress> promise, DnsCache resolveCache)
    {
        DnsServerAddressStream dnsServerAddressStream = dnsServerAddressStreamProvider
                .nameServerAddressStream(hostname);
        SingleResolverContext ctx = dnsServerAddressStream == null
                ? new SingleResolverContext(this, hostname, additionals,
                        resolveCache, nameServerAddresses.stream())
                : new SingleResolverContext(this, hostname, additionals,
                        resolveCache, dnsServerAddressStream);
        ctx.resolve(promise);
    }

    static final class SingleResolverContext
            extends DnsNameResolverContext<InetAddress>
    {
        SingleResolverContext(DnsNameResolver parent, String hostname,
                DnsRecord[] additionals, DnsCache resolveCache,
                DnsServerAddressStream nameServerAddrs)
        {
            super(parent, hostname, additionals, resolveCache, nameServerAddrs);
        }

        @Override
        DnsNameResolverContext<InetAddress> newResolverContext(
                DnsNameResolver parent, String hostname,
                DnsRecord[] additionals, DnsCache resolveCache,
                DnsServerAddressStream nameServerAddrs)
        {
            return new SingleResolverContext(parent, hostname, additionals,
                    resolveCache, nameServerAddrs);
        }

        @Override
        boolean finishResolve(Class<? extends InetAddress> addressType,
                List<DnsCacheEntry> resolvedEntries,
                Promise<InetAddress> promise)
        {

            final int numEntries = resolvedEntries.size();
            for (int i = 0; i < numEntries; i++)
            {
                final InetAddress a = resolvedEntries.get(i).address();
                if (addressType.isInstance(a))
                {
                    trySuccess(promise, a);
                    return true;
                }
            }
            return false;
        }
    }

    @Override
    protected void doResolveAll(String inetHost,
            Promise<List<InetAddress>> promise) throws Exception
    {
        doResolveAll(inetHost, EMPTY_ADDITIONALS, promise, resolveCache);
    }

    /**
     * Hook designed for extensibility so one can pass a different cache on each
     * resolution attempt instead of using the global one.
     */
    protected void doResolveAll(String inetHost, DnsRecord[] additionals,
            Promise<List<InetAddress>> promise, DnsCache resolveCache)
            throws Exception
    {
        if (inetHost == null || inetHost.isEmpty())
        {
            // If an empty hostname is used we should use "localhost", just like
            // InetAddress.getAllByName(...) does.
            promise.setSuccess(Collections.singletonList(loopbackAddress()));
            return;
        }
        final byte[] bytes = NetUtil
                .createByteArrayFromIpAddressString(inetHost);
        if (bytes != null)
        {
            // The unresolvedAddress was created via a String that contains an
            // ipaddress.
            promise.setSuccess(
                    Collections.singletonList(InetAddress.getByAddress(bytes)));
            return;
        }

        final String hostname = hostname(inetHost);

        InetAddress hostsFileEntry = resolveHostsFileEntry(hostname);
        if (hostsFileEntry != null)
        {
            promise.setSuccess(Collections.singletonList(hostsFileEntry));
            return;
        }

        if (!doResolveAllCached(hostname, additionals, promise, resolveCache))
        {
            doResolveAllUncached(hostname, additionals, promise, resolveCache);
        }
    }

    private boolean doResolveAllCached(String hostname, DnsRecord[] additionals,
            Promise<List<InetAddress>> promise, DnsCache resolveCache)
    {
        final List<DnsCacheEntry> cachedEntries = resolveCache.get(hostname,
                additionals);
        if (cachedEntries == null || cachedEntries.isEmpty())
        {
            return false;
        }

        List<InetAddress> result = null;
        Throwable cause = null;
        synchronized (cachedEntries)
        {
            final int numEntries = cachedEntries.size();
            assert numEntries > 0;

            if (cachedEntries.get(0).cause() != null)
            {
                cause = cachedEntries.get(0).cause();
            }
            else
            {
                for (InternetProtocolFamily f : resolvedInternetProtocolFamilies)
                {
                    for (int i = 0; i < numEntries; i++)
                    {
                        final DnsCacheEntry e = cachedEntries.get(i);
                        if (f.addressType().isInstance(e.address()))
                        {
                            if (result == null)
                            {
                                result = new ArrayList<InetAddress>(numEntries);
                            }
                            result.add(e.address());
                        }
                    }
                }
            }
        }

        if (result != null)
        {
            trySuccess(promise, result);
            return true;
        }
        if (cause != null)
        {
            tryFailure(promise, cause);
            return true;
        }
        return false;
    }

    static final class ListResolverContext
            extends DnsNameResolverContext<List<InetAddress>>
    {
        ListResolverContext(DnsNameResolver parent, String hostname,
                DnsRecord[] additionals, DnsCache resolveCache,
                DnsServerAddressStream nameServerAddrs)
        {
            super(parent, hostname, additionals, resolveCache, nameServerAddrs);
        }

        @Override
        DnsNameResolverContext<List<InetAddress>> newResolverContext(
                DnsNameResolver parent, String hostname,
                DnsRecord[] additionals, DnsCache resolveCache,
                DnsServerAddressStream nameServerAddrs)
        {
            return new ListResolverContext(parent, hostname, additionals,
                    resolveCache, nameServerAddrs);
        }

        @Override
        boolean finishResolve(Class<? extends InetAddress> addressType,
                List<DnsCacheEntry> resolvedEntries,
                Promise<List<InetAddress>> promise)
        {

            List<InetAddress> result = null;
            final int numEntries = resolvedEntries.size();
            for (int i = 0; i < numEntries; i++)
            {
                final InetAddress a = resolvedEntries.get(i).address();
                if (addressType.isInstance(a))
                {
                    if (result == null)
                    {
                        result = new ArrayList<InetAddress>(numEntries);
                    }
                    result.add(a);
                }
            }

            if (result != null)
            {
                promise.trySuccess(result);
                return true;
            }
            return false;
        }
    }

    private void doResolveAllUncached(String hostname, DnsRecord[] additionals,
            Promise<List<InetAddress>> promise, DnsCache resolveCache)
    {
        DnsServerAddressStream dnsServerAddressStream = dnsServerAddressStreamProvider
                .nameServerAddressStream(hostname);
        ListResolverContext ctx = dnsServerAddressStream == null
                ? new ListResolverContext(this, hostname, additionals,
                        resolveCache, nameServerAddresses.stream())
                : new ListResolverContext(this, hostname, additionals,
                        resolveCache, dnsServerAddressStream);
        ctx.resolve(promise);
    }

    private static String hostname(String inetHost)
    {
        String hostname = IDN.toASCII(inetHost);
        // Check for http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6894622
        if (StringUtil.endsWith(inetHost, '.')
                && !StringUtil.endsWith(hostname, '.'))
        {
            hostname += ".";
        }
        return hostname;
    }

    /**
     * Sends a DNS query with the specified question.
     */
    public Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query(
            DnsQuestion question)
    {
        return query(nextNameServerAddress(), question);
    }

    /**
     * Sends a DNS query with the specified question with additional records.
     */
    public Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query(
            DnsQuestion question, Iterable<DnsRecord> additionals)
    {
        return query(nextNameServerAddress(), question, additionals);
    }

    /**
     * Sends a DNS query with the specified question.
     */
    public Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query(
            DnsQuestion question,
            Promise<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> promise)
    {
        return query(nextNameServerAddress(), question,
                Collections.<DnsRecord> emptyList(), promise);
    }

    private InetSocketAddress nextNameServerAddress()
    {
        return nameServerAddrStream.get().next();
    }

    /**
     * Sends a DNS query with the specified question using the specified name
     * server list.
     */
    public Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query(
            InetSocketAddress nameServerAddr, DnsQuestion question)
    {

        return query0(nameServerAddr, question, EMPTY_ADDITIONALS, ch
                .eventLoop()
                .<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> newPromise());
    }

    /**
     * Sends a DNS query with the specified question with additional records
     * using the specified name server list.
     */
    public Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query(
            InetSocketAddress nameServerAddr, DnsQuestion question,
            Iterable<DnsRecord> additionals)
    {

        return query0(nameServerAddr, question, toArray(additionals, false), ch
                .eventLoop()
                .<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> newPromise());
    }

    /**
     * Sends a DNS query with the specified question using the specified name
     * server list.
     */
    public Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query(
            InetSocketAddress nameServerAddr, DnsQuestion question,
            Promise<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> promise)
    {

        return query0(nameServerAddr, question, EMPTY_ADDITIONALS, promise);
    }

    /**
     * Sends a DNS query with the specified question with additional records
     * using the specified name server list.
     */
    public Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query(
            InetSocketAddress nameServerAddr, DnsQuestion question,
            Iterable<DnsRecord> additionals,
            Promise<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> promise)
    {

        return query0(nameServerAddr, question, toArray(additionals, false),
                promise);
    }

    Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query0(
            InetSocketAddress nameServerAddr, DnsQuestion question,
            DnsRecord[] additionals,
            Promise<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> promise)
    {

        final Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> castPromise = cast(
                checkNotNull(promise, "promise"));
        try
        {
            new DnsQueryContext(this, nameServerAddr, question, additionals,
                    castPromise).query();
            return castPromise;
        }
        catch (Exception e)
        {
            return castPromise.setFailure(e);
        }
    }

    @SuppressWarnings("unchecked")
    private static Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> cast(
            Promise<?> promise)
    {
        return (Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>>) promise;
    }

    private final class DnsResponseHandler extends ChannelInboundHandlerAdapter
    {

        private final Promise<Channel> channelActivePromise;

        DnsResponseHandler(Promise<Channel> channelActivePromise)
        {
            this.channelActivePromise = channelActivePromise;
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg)
                throws Exception
        {
            try
            {
                final DatagramDnsResponse res = (DatagramDnsResponse) msg;
                final int queryId = res.id();

                if (logger.isDebugEnabled())
                {
                    logger.debug("{} RECEIVED: [{}: {}], {}", ch, queryId,
                            res.sender(), res);
                }

                final DnsQueryContext qCtx = queryContextManager
                        .get(res.sender(), queryId);
                if (qCtx == null)
                {
                    logger.warn(
                            "{} Received a DNS response with an unknown ID: {}",
                            ch, queryId);
                    return;
                }

                qCtx.finish(res);
            }
            finally
            {
                ReferenceCountUtil.safeRelease(msg);
            }
        }

        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception
        {
            super.channelActive(ctx);
            channelActivePromise.setSuccess(ctx.channel());
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
                throws Exception
        {
            logger.warn("{} Unexpected exception: ", ch, cause);
        }
    }
}
